Гэта апошняя артыкул у серыі «Чыстыя коды ў ходзе». Папярэднія часткі: Clean Code: Functions and Error Handling in Go: From Chaos to Clarity (Частка 1) Чысты код у ходзе (частка 2): структура, метады і склад над спадчыннем Clean Code: Interfaces in Go - Why Small Is Beautiful [Частка 3] Clean Code in Go (Частка 4): Пакетная архітэктура, залежны поток і скалабленне Галоўная ➠ Публікацыі ➠ На каранцін зачынена самая шматлікая школа Бутурлінауцы Я адбудаваў уплывы горуціны ў 3 раніцы, фіксаваў рэальныя ўмовы, якія з'явіліся толькі пад нагрузкай, і глядзеў на адну няўдачу «Не размаўляйце, размаўляючы з памяшканнем; размаўляйце з памяшканнем, размаўляючы» — гэта мантра Го змяніла паралельнае праграмаванне на яе галаве. defer Звычайныя прыкметы, з якімі я ставіўся: Goroutine Leaks: ~40% праблем з памяшканнем вытворчасці Умовы расы з распаўсюджаным станам: ~35% сумеснага кода Абмежаванне кантэксту: ~50% відэму часу Deadlocks ад злоўжывання каналаў: ~25% вывешаных паслуг Няправільнае выкарыстанне мутэкса (прыёмнік значэнняў): ~30% няправільных сінкцый Варта адзначыць, што для ажыццяўлення работ па стварэнні Нацыянальнага інвентара нематэрыяльнай культурнай спадчыны наша краіна летась атрымала грант з адпаведнага фонду UNESCO. Вынікі пошуку - life cycle management Першая частка контекста // RULE: context.Context is ALWAYS the first parameter func GetUser(ctx context.Context, userID string) (*User, error) { // correct } func GetUser(userID string, ctx context.Context) (*User, error) { // wrong - violates convention } Скасаванне // BAD: operation cannot be cancelled func SlowOperation() (Result, error) { time.Sleep(10 * time.Second) // always waits 10 seconds return Result{}, nil } // GOOD: operation respects context func SlowOperation(ctx context.Context) (Result, error) { select { case <-time.After(10 * time.Second): return Result{}, nil case <-ctx.Done(): return Result{}, ctx.Err() } } // Usage with timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() result, err := SlowOperation(ctx) if err == context.DeadlineExceeded { log.Println("Operation timed out") } Папярэдні Тэкст: Зрабіць гэта варта! // BAD: using context for business logic type key string const userKey key = "user" func WithUser(ctx context.Context, user *User) context.Context { return context.WithValue(ctx, userKey, user) } func GetUser(ctx context.Context) *User { return ctx.Value(userKey).(*User) // panic if no user! } // GOOD: context only for request metadata type contextKey string const ( requestIDKey contextKey = "requestID" traceIDKey contextKey = "traceID" ) func WithRequestID(ctx context.Context, requestID string) context.Context { return context.WithValue(ctx, requestIDKey, requestID) } func GetRequestID(ctx context.Context) string { if id, ok := ctx.Value(requestIDKey).(string); ok { return id } return "" } // BETTER: explicit parameter passing func ProcessOrder(ctx context.Context, user *User, order *Order) error { // user passed explicitly, not through context return nil } Навiны по тэме: Легкая конкуренция Вынікі пошуку - worker pool // Worker pool to limit concurrency type WorkerPool struct { workers int jobs chan Job results chan Result wg sync.WaitGroup } type Job struct { ID int Data []byte } type Result struct { JobID int Output []byte Error error } func NewWorkerPool(workers int) *WorkerPool { return &WorkerPool{ workers: workers, jobs: make(chan Job, workers*2), results: make(chan Result, workers*2), } } func (p *WorkerPool) Start(ctx context.Context) { for i := 0; i < p.workers; i++ { p.wg.Add(1) go p.worker(ctx, i) } } func (p *WorkerPool) worker(ctx context.Context, id int) { defer p.wg.Done() for { select { case job, ok := <-p.jobs: if !ok { return } result := p.processJob(job) select { case p.results <- result: case <-ctx.Done(): return } case <-ctx.Done(): return } } } func (p *WorkerPool) processJob(job Job) Result { // Process job output := bytes.ToUpper(job.Data) return Result{ JobID: job.ID, Output: output, } } func (p *WorkerPool) Submit(job Job) { p.jobs <- job } func (p *WorkerPool) Shutdown() { close(p.jobs) p.wg.Wait() close(p.results) } // Usage func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() pool := NewWorkerPool(10) pool.Start(ctx) // Submit jobs for i := 0; i < 100; i++ { pool.Submit(Job{ ID: i, Data: []byte(fmt.Sprintf("job-%d", i)), }) } // Collect results go func() { for result := range pool.results { log.Printf("Result %d: %s", result.JobID, result.Output) } }() // Graceful shutdown pool.Shutdown() } Асноўны артыкул: Fan-out / Fan-in // Fan-out: distribute work among goroutines func fanOut(ctx context.Context, in <-chan int, workers int) []<-chan int { outputs := make([]<-chan int, workers) for i := 0; i < workers; i++ { output := make(chan int) outputs[i] = output go func() { defer close(output) for { select { case n, ok := <-in: if !ok { return } // Heavy work result := n * n select { case output <- result: case <-ctx.Done(): return } case <-ctx.Done(): return } } }() } return outputs } // Fan-in: collect results from goroutines func fanIn(ctx context.Context, inputs ...<-chan int) <-chan int { output := make(chan int) var wg sync.WaitGroup for _, input := range inputs { wg.Add(1) go func(ch <-chan int) { defer wg.Done() for { select { case n, ok := <-ch: if !ok { return } select { case output <- n: case <-ctx.Done(): return } case <-ctx.Done(): return } } }(input) } go func() { wg.Wait() close(output) }() return output } // Usage func pipeline(ctx context.Context) { // Number generator numbers := make(chan int) go func() { defer close(numbers) for i := 1; i <= 100; i++ { select { case numbers <- i: case <-ctx.Done(): return } } }() // Fan-out to 5 workers workers := fanOut(ctx, numbers, 5) // Fan-in results results := fanIn(ctx, workers...) // Process results for result := range results { fmt.Printf("Result: %d\n", result) } } Каналы: Грамадзяне першага класа Рухавыя каналы // BAD: bidirectional channel everywhere func producer(ch chan int) { ch <- 42 } func consumer(ch chan int) { value := <-ch } // GOOD: restrict direction func producer(ch chan<- int) { // send-only ch <- 42 } func consumer(ch <-chan int) { // receive-only value := <-ch } // Compiler will check correct usage func main() { ch := make(chan int) go producer(ch) go consumer(ch) } Выбраныя і неблокаваныя аперацыі // Pattern: timeout with select func RequestWithTimeout(url string, timeout time.Duration) ([]byte, error) { result := make(chan []byte, 1) errCh := make(chan error, 1) go func() { resp, err := http.Get(url) if err != nil { errCh <- err return } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { errCh <- err return } result <- data }() select { case data := <-result: return data, nil case err := <-errCh: return nil, err case <-time.After(timeout): return nil, fmt.Errorf("request timeout after %v", timeout) } } // Non-blocking send func TrySend(ch chan<- int, value int) bool { select { case ch <- value: return true default: return false // channel full } } // Non-blocking receive func TryReceive(ch <-chan int) (int, bool) { select { case value := <-ch: return value, true default: return 0, false // channel empty } } Расы ўмовы і як іх унесці Вынікі пошуку - data race // DANGEROUS: data race type Counter struct { value int } func (c *Counter) Inc() { c.value++ // NOT atomic! } func (c *Counter) Value() int { return c.value // race on read } // Check: go test -race Рашэнне 1: Mutex type SafeCounter struct { mu sync.RWMutex value int } func (c *SafeCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.value++ } func (c *SafeCounter) Value() int { c.mu.RLock() defer c.mu.RUnlock() return c.value } // Pattern: protecting invariants type BankAccount struct { mu sync.Mutex balance decimal.Decimal } func (a *BankAccount) Transfer(to *BankAccount, amount decimal.Decimal) error { // Important: always lock in same order (by ID) // to avoid deadlock if a.ID() < to.ID() { a.mu.Lock() defer a.mu.Unlock() to.mu.Lock() defer to.mu.Unlock() } else { to.mu.Lock() defer to.mu.Unlock() a.mu.Lock() defer a.mu.Unlock() } if a.balance.LessThan(amount) { return ErrInsufficientFunds } a.balance = a.balance.Sub(amount) to.balance = to.balance.Add(amount) return nil } Рашэнне 2: Каналы для сіндронізацыі // Use channels instead of mutexes type ChannelCounter struct { ch chan countOp } type countOp struct { delta int resp chan int } func NewChannelCounter() *ChannelCounter { c := &ChannelCounter{ ch: make(chan countOp), } go c.run() return c } func (c *ChannelCounter) run() { value := 0 for op := range c.ch { value += op.delta if op.resp != nil { op.resp <- value } } } func (c *ChannelCounter) Inc() { c.ch <- countOp{delta: 1} } func (c *ChannelCounter) Value() int { resp := make(chan int) c.ch <- countOp{resp: resp} return <-resp } Конкурентныя мадэлі Альтэрнатыўныя назвы: Graceful Shutdown type Server struct { server *http.Server shutdown chan struct{} done chan struct{} } func NewServer(addr string) *Server { return &Server{ server: &http.Server{ Addr: addr, }, shutdown: make(chan struct{}), done: make(chan struct{}), } } func (s *Server) Start() { go func() { defer close(s.done) if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("Server error: %v", err) } }() // Wait for shutdown signal go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) select { case <-sigCh: case <-s.shutdown: } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := s.server.Shutdown(ctx); err != nil { log.Printf("Shutdown error: %v", err) } }() } func (s *Server) Stop() { close(s.shutdown) <-s.done } Вынікі пошуку - limit type RateLimiter struct { rate int bucket chan struct{} stop chan struct{} } func NewRateLimiter(rate int) *RateLimiter { rl := &RateLimiter{ rate: rate, bucket: make(chan struct{}, rate), stop: make(chan struct{}), } // Fill bucket for i := 0; i < rate; i++ { rl.bucket <- struct{}{} } // Refill bucket at given rate go func() { ticker := time.NewTicker(time.Second / time.Duration(rate)) defer ticker.Stop() for { select { case <-ticker.C: select { case rl.bucket <- struct{}{}: default: // bucket full } case <-rl.stop: return } } }() return rl } func (rl *RateLimiter) Allow() bool { select { case <-rl.bucket: return true default: return false } } func (rl *RateLimiter) Wait(ctx context.Context) error { select { case <-rl.bucket: return nil case <-ctx.Done(): return ctx.Err() } } Памер: труба з апраўданням пашкоджанняў // Pipeline stage with error handling type Stage func(context.Context, <-chan int) (<-chan int, <-chan error) // Compose stages func Pipeline(ctx context.Context, stages ...Stage) (<-chan int, <-chan error) { var ( dataOut = make(chan int) errOut = make(chan error) dataIn <-chan int errIn <-chan error ) // Start generator start := make(chan int) go func() { defer close(start) for i := 1; i <= 100; i++ { select { case start <- i: case <-ctx.Done(): return } } }() dataIn = start // Apply stages for _, stage := range stages { dataIn, errIn = stage(ctx, dataIn) // Collect errors go func(errors <-chan error) { for err := range errors { select { case errOut <- err: case <-ctx.Done(): return } } }(errIn) } // Final output go func() { defer close(dataOut) for val := range dataIn { select { case dataOut <- val: case <-ctx.Done(): return } } }() return dataOut, errOut } Практычныя тыпы Звычайна выкарыстоўвайце кантэкст для доўгіх аперацый Не робіце гаруціны без кантролю — выкарыстоўвайце рабочыя бальніцы Прафесійныя каналы для кандыдатаў Выкарыстанне sync/atomic для простых лічбаў Загрузіць бясплатную бясплатную бясплатную бясплатную бясплатную бясплатную бясплатную бясплатную бясплатную бясплатную бягу Замежныя каналы Увесь час думаюць пра ганаровае зачыненне Конкурентныя чэкі Папярэдні Тэкст Першы параметр Гуртыны могуць быць спыненыя праз кантэкст Ніколі не загінуў (не загінуў) Каналы, зачыненыя адпраўляльнікам Мутэкс зачынены ў тым жа парадку Вынікі пошуку - race flag Заўсёды цікава закрыць Працоўны басейн для буйных аперацый Высновы У нас функцыянуюць таварыствы “Разумнікі і разумніцы”, “Даследчык”, а таксама адзіная ў Магілёўскай вобласці астранамічная пляцоўка. Гэты артыкул завяршае серыю "Чысты код у Go".Мы ацанілі шлях ад функцый да сумеснасці, трымаючы ўсе ключавыя аспекты напісання ідыматычнага кода Go.Памятайце: Go - гэта пра простасць, а чысты код у Go - гэта код, які выконвае ідымомы мовы. Які ваш найгоршы вытворчы інцыдэнт, выкліканы расавымі ўмовамі? Як вы праверыце паралельны код? Якія мадэлі захавалі вас ад выпадак гаруціны? Падзяліцеся вашымі гісторыі вайны ў каментарах!